1 /** 2 Keyed item is used in combination with KeyedCollection to mimic databases 3 in your classes. This module contains: 4 $(TOC KeyedItem) 5 6 License: $(GPL2) 7 8 Authors: Matthew Armbruster 9 10 $(B Source:) $(SRC $(SRCFILENAME)) 11 12 Copyright: 2016 13 */ 14 module db_constraints.keyed.keyeditem; 15 16 import std.traits : isInstanceOf; 17 18 public import db_constraints.constraints; 19 import db_constraints.utils.meta : hasMembersWithUDA; 20 21 /** 22 Use this in the singular class which would describe a row in your 23 database. ClusteredIndexAttribute is the unique constraint associated 24 with the clustered index. 25 */ 26 mixin template KeyedItem(ClusteredIndexAttribute = PrimaryKeyColumn) 27 if (isInstanceOf!(UniqueConstraintColumn, ClusteredIndexAttribute)) 28 { 29 import std.algorithm : among, canFind; 30 import std.conv : to; 31 import std.exception : collectException, enforceEx; 32 import std.functional : unaryFun; 33 import std.meta : Erase; 34 import std.signals; 35 import std.string : lastIndexOf; 36 import std.traits : isInstanceOf; 37 38 import db_constraints.db_exceptions : CheckConstraintException; 39 import db_constraints.utils.meta; 40 41 final private alias T = typeof(this); 42 private bool _containsChanges; 43 private ClusteredIndex _key; 44 45 static assert(hasMembersWithUDA!(T, ClusteredIndexAttribute), 46 "Must have columns with @UniqueConstraintColumn!\"" ~ 47 ClusteredIndexAttribute.name ~ "\" to use this mixin."); 48 49 /** 50 The setter should be in your setter member. This checks your check 51 constraint and notifies the item if it is different and does not 52 violate the check constraint. 53 54 $(THROWS CheckConstraintException, if your value makes checkConstraints fail.) 55 */ 56 final private void setter(P)(ref P member, P value, string name_ = __FUNCTION__) 57 { 58 if (value != member) 59 { 60 P memberValue = member; 61 member = value; 62 auto ex = collectException!CheckConstraintException(checkConstraints()); 63 if (ex is null) 64 { 65 string name = name_[lastIndexOf(name_, '.') + 1 .. $]; 66 notify(name); 67 } 68 else 69 { 70 member = memberValue; 71 throw ex; 72 } 73 } 74 } 75 /** 76 Initializes the keyed item by running $(SRCTAG setClusteredIndex) and 77 $(SRCTAG checkConstraints). 78 This should be in your constructor. 79 80 $(THROWS CheckConstraintException, if a member violates their constraint.) 81 */ 82 final private void initializeKeyedItem() 83 { 84 setClusteredIndex(); 85 checkConstraints(); 86 } 87 88 89 /** 90 Read-only property telling if $(D this) contains changes. 91 Returns: 92 true if $(D this) contains changes. 93 */ 94 final @property bool containsChanges() const nothrow pure @safe @nogc 95 { 96 return _containsChanges; 97 } 98 /** 99 Changes $(D this) to not contain changes. Should only 100 be used after a save. 101 */ 102 final void markAsSaved() nothrow pure @safe @nogc 103 { 104 _containsChanges = false; 105 } 106 /** 107 The signal used to emit changes that occur in $(D this). 108 */ 109 mixin Signal!(string, typeof(_key)) emitChange; 110 111 /** 112 Notifies $(D this) which property changed. If the property is 113 part of the clustered index then the clustered index is updated. 114 This also emits a signal with the property name that changed 115 along with the clustered index. 116 Params: 117 propertyName = the property name that changed 118 */ 119 final void notify(string propertyName) 120 { 121 _containsChanges = true; 122 emitChange.emit(propertyName, _key); 123 if (GetMembersWithUDA!(T, ClusteredIndexAttribute).canFind(propertyName)) 124 { 125 emitChange.emit("key", _key); 126 setClusteredIndex(); 127 } 128 foreach(name; Erase!(ClusteredIndexAttribute.name, GetUniqueConstraintStructNames!(T))) 129 { 130 if (GetMembersWithUDA!(T, UniqueConstraintColumn!name).canFind(propertyName)) 131 { 132 emitChange.emit(name ~ "_key", _key); 133 } 134 } 135 foreach(fk; GetForeignKeys!(T)) 136 { 137 if (fk.columnNames.canFind(propertyName)) 138 { 139 emitChange.emit(fk.name ~ "_key", _key); 140 } 141 } 142 } 143 /** 144 Checks if any of the members of $(D T) have values that violate their 145 check constraint. 146 147 $(THROWS CheckConstraintException, if the constraint is violated.) 148 */ 149 final void checkConstraints() 150 { 151 foreach(member; __traits(derivedMembers, T)) 152 { 153 static if (__traits(compiles, __traits(getMember, T, member))) 154 { 155 foreach(ov; __traits(getOverloads, T, member)) 156 { 157 foreach(attr; __traits(getAttributes, ov)) 158 { 159 static if (isInstanceOf!(CheckConstraint, attr)) 160 { 161 static if (attr.name.among!("NotNull", "Set", "Enum")) 162 { 163 enum msg = T.stringof ~ "." ~ member ~ 164 " " ~ attr.name ~ " violation."; 165 166 } 167 else static if (attr.name == "") 168 { 169 enum msg = "chk_" ~ T.stringof ~ "_" ~ member ~ 170 " violation."; 171 } 172 else 173 { 174 enum msg = attr.name ~ " violation."; 175 } 176 enforceEx!(CheckConstraintException)( 177 attr.check(mixin("this._" ~ member)), msg); 178 } 179 } 180 } 181 } 182 } 183 foreach(attr; __traits(getAttributes, T)) 184 { 185 static if (isInstanceOf!(CheckConstraint, attr)) 186 { 187 static if (attr.name.among!("NotNull", "Set", "Enum")) 188 { 189 enum msg = T.stringof ~ " " ~ attr.name ~ " violation."; 190 191 } 192 else static if (attr.name == "") 193 { 194 enum msg = "chk_" ~ T.stringof ~ 195 " violation."; 196 } 197 else 198 { 199 enum msg = attr.name ~ " violation."; 200 } 201 enforceEx!(CheckConstraintException)( 202 attr.check(this), msg); 203 } 204 } 205 } 206 207 /** 208 Clustered index struct created at compile-time. 209 This is used to compare classes. The members 210 are the members of the class marked with the 211 attribute selected as the Clustered Index. 212 */ 213 final struct ClusteredIndex 214 { 215 // creates the members of the clustered key with appropriate type. 216 mixin(function string() 217 { 218 string result = ""; 219 foreach(columnName; GetMembersWithUDA!(T, ClusteredIndexAttribute)) 220 { 221 result ~= "typeof(" ~ T.stringof ~ "._" ~ columnName ~ ") " ~ columnName ~ ";\n"; 222 } 223 return result; 224 }()); 225 // adds the generic comparison for structs 226 mixin opAAKey!(ClusteredIndex); 227 } 228 229 230 /** 231 The clustered index property for the class. 232 Returns: 233 The clustered index for the class. 234 */ 235 final @property ClusteredIndex key() const nothrow pure @safe @nogc 236 { 237 return _key; 238 } 239 240 /** 241 Sets the clustered index for $(D this). 242 */ 243 final void setClusteredIndex() nothrow pure @safe @nogc 244 { 245 auto new_key = ClusteredIndex(); 246 mixin(function string() 247 { 248 string result = ""; 249 foreach(pkcolumn; GetMembersWithUDA!(T, ClusteredIndexAttribute)) 250 { 251 result ~= "new_key." ~ pkcolumn ~ " = this._" ~ pkcolumn ~ ";\n"; 252 } 253 return result; 254 }()); 255 this._key = new_key; 256 } 257 258 static if (hasForeignKeys!(T)) 259 { 260 mixin(createForeignKeyPropertyConverter!(T)); 261 } 262 263 mixin(createConstraintStructs!(T, ClusteredIndexAttribute.name)); 264 } 265 266 /// 267 unittest 268 { 269 class Candy 270 { 271 private: 272 string _name; 273 int _ranking; 274 public: 275 // name is the primary key 276 @PrimaryKeyColumn @NotNull 277 @property string name() const nothrow pure @safe @nogc 278 { 279 return _name; 280 } 281 @property void name(string value) 282 { 283 setter(_name, value); 284 } 285 // ranking must be unique among all the other records 286 @UniqueConstraintColumn!("uc_Candy_ranking") 287 @property int ranking() const nothrow pure @safe @nogc 288 { 289 return _ranking; 290 } 291 // making sure that ranking will always be above 0 292 @CheckConstraint!(a => a > 0, "chk_Candy_ranking") 293 @property void ranking(int value) 294 { 295 setter(_ranking, value); 296 } 297 this(string name, int ranking) 298 { 299 this._name = name; 300 this._ranking = ranking; 301 initializeKeyedItem(); 302 } 303 304 // The primary key is now the clustered index as it is by default 305 mixin KeyedItem!(PrimaryKeyColumn); 306 } 307 308 // below is what is created when you include the mixin KeyedItem 309 // ClusteredIndex is alias'd as PrimaryKey since we said the 310 // primary key is our clustered index above. 311 // this also creates a uc_Candy_ranking struct and key since 312 // we labeled ranking with @UniqueConstraintColumn!("uc_Candy_ranking") 313 enum candyStructs = 314 `public: 315 final alias PrimaryKey = ClusteredIndex; 316 final alias PrimaryKey_key = key; 317 final struct uc_Candy_ranking 318 { 319 typeof(Candy._ranking) ranking; 320 mixin opAAKey!(uc_Candy_ranking); 321 } 322 final @property uc_Candy_ranking uc_Candy_ranking_key() const nothrow pure @safe @nogc 323 { 324 auto _uc_Candy_ranking_key = uc_Candy_ranking(); 325 _uc_Candy_ranking_key.ranking = this._ranking; 326 return _uc_Candy_ranking_key; 327 } 328 `; 329 import db_constraints.utils.meta : createConstraintStructs; 330 static assert(createConstraintStructs!(Candy, "PrimaryKey") == candyStructs); 331 assert(createConstraintStructs!(Candy, "PrimaryKey") == candyStructs); 332 333 334 // source: http://www.bloomberg.com/ss/09/10/1021_americas_25_top_selling_candies/10.htm 335 auto i = new Candy("Opal Fruit", 17); 336 337 // i does not contain changes 338 assert(!i.containsChanges); 339 340 auto pk = Candy.PrimaryKey("Opal Fruit"); 341 // the key property is the clustered index 342 // since we said the primary key is the clustered index 343 // i.key and pk are equal 344 assert(i.key == pk); 345 // PrimaryKey_key is an alias for key 346 assert(i.key == i.PrimaryKey_key); 347 // the primary key struct has member name since that was marked 348 // with @PrimaryKeyColumn 349 assert(i.key.name == pk.name); 350 assert(i.name == pk.name); 351 352 auto j = new Candy("Opal Fruit", 16); 353 // since name is the primary key i and j are equal because 354 // the names are equal 355 // even though the ranking is different 356 assert(i.key == j.key); 357 assert(i.ranking != j.ranking); 358 359 // in 1967 Opal Fruits came to America and changed its name 360 i.name = "Starburst"; 361 // i now contains changes since we changed the name 362 assert(i.containsChanges); 363 i.markAsSaved(); 364 // once we mark it as saved it no longer contains changes 365 assert(!i.containsChanges); 366 367 // by changing the name it also changes the primary key 368 // so now i.key should not equal the pk we defined above 369 // or j.key 370 assert(i.key != pk); 371 assert(i.key != j.key); 372 373 import std.exception : assertThrown; 374 import db_constraints.db_exceptions : CheckConstraintException; 375 // we expect setting the ranking to 0 will result in an exception 376 // since we labeled that column with 377 // @CheckConstraint!(a => a > 0, "chk_Candy_ranking") 378 assertThrown!CheckConstraintException(i.ranking = 0); 379 }